package cn.itcast.nio.c2;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static cn.itcast.nio.c1.ByteBufferUtil.debugAll;
import static cn.itcast.nio.c1.ByteBufferUtil.debugRead;

/**
 * netty服务端
 * 使用selector来实现 Server 非阻塞 且 能防止cpu空转 演示
 * 利用selector 则单线程就能直接完成多个channel的可读写事件的监控  这就称之为多路复用
 * 我们java中说的一个英文啥的占一个字节  一个中文汉字占 2个字节 是因为java中默认是用unicode编码的
 * 但是如果我们用其他的编码方式 那么久可能会导致一个中文可能三个字节这样 比如utf-8
 *
 * 记住netty本质的原理都是通过事件来区分，然后通过不同的事件key(服务端/客户端)就可以拿到不用的对应的channel(服务端/客户端)
 * 然后就可以从channel中读/写数据了
 *
 *这也可以算是一个redis的小缺点吧如下
 * redis内部也是使用的类似selector这种的网络结构设计模式，所以redis的网络模型也是单线程的多路复用的
 * 所以我们使用redis的时候 要尽量避免单独的一个工作可能耗时太长，因为这样可能会对其他的线程产生影响
 * netty就是多线程的selector的方式来做的可以这么理解，有个selector线程去负责accept事件然后
 * 读写去分给其他的selector线程让它们去调度执行读写操作，
 *
 */
@Slf4j
public class SelectorServer {
    public static void main(String[] args) throws IOException {

        //1.创建selector 一个selector它可以管理多个channel (我们可以服务端跟客户端共用)
        Selector selector = Selector.open();
        //打开服务端的通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //设置服务端为非阻塞
        ssc.configureBlocking(false);
        //服务端绑定端口号为8080
        ssc.bind(new InetSocketAddress(8080));

        // 2.建立 selector 和 channel 的联系(注册)
        // 这里这个sscKey就是管理服务器channel的了
        // SelectionKey 就是将来事件发生后，通过它可以知道是哪个事件 跟 是哪个channel发生的事件
        // SelectionKey 里面有四种事件 如下
        // accept: 有连接请求的时候触发常用来接受连接  connect: 建立连接后触发   服务端关注这两个事件
        // read: 可读事件  write: 可写事件              客户端关注这两个事件
        SelectionKey sscKey = ssc.register(selector, 0, null);

        log.debug("sscKey:{}",sscKey);
        //所以对于我们服务端的channel只需要关注accept即可 - 来关注客户端的连接
        sscKey.interestOps(SelectionKey.OP_ACCEPT);


        while (true){
            //3.接受事件 会把接收到的事件放到selector.selectedKeys()这个集合中
            //没有事件(上面的四种事件任意之一)发生时线程会阻塞该行代码会卡住,当有事件发生时则往下运行
            //如果获取的事件没被处理掉(没用accept/connect/read/write)，他会重新把这个事件放回去继续让你处理 非阻塞继续往下执行
            //所以其实总结就还是那句话 没事件就阻塞卡住等事件来，事件发生后要么处理要么取消不能置之不理
            selector.select();
            //selector.select(long xxx);  //阻塞直到绑定事件发生，或是超时(单位是ms)
            //selector.selectNow();         //立马返回，自己去根据返回值判断有没有事件
            //selector.wakeup();            //唤醒阻塞在select()上的线程


            //4.处理事件
            //从selector.selectedKeys()中拿到所有 可读 可写 可连接 连接后 触发的事件集合
            //比如有两个客户端连接上来了 那么集合这里面就有2个key 这里是可以所有客户端的事件一起管理的 但是每个客户端可能做不同的事
            //比如有的客户端是连接 有的客户端则是准备发数据等 所以得区分事件类型
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();  //在集合遍历的时候还想进行删除操作就得用迭代器来操作
            while (iterator.hasNext()){
                //这里的key就是连接上服务器的客户端的事件key
                //我们就可以通过这个key来使用accept方法 因为在上面我们关注了accept
                SelectionKey key = iterator.next();

                //处理完成一个key就得立马移除掉这个key，这里的移除可以理解为逻辑删,集合中还有只是我们不会直接取到了
                //因为selector.selectedKeys()集合它在处理完一个Key后是不会自动移除这个key的
                //所以如果不自己手动移除的话，下次这个key还是存在 但下次的时候它已经是被处理过的，就会导致空指针异常
                //在这个方法内还是有这个key的引用的所以先执行remove也没事，在代码最后写也可以
                iterator.remove();

                //key一直都是同个key 但是channel是不同的channel
                //这个key 都是从selector来的 但是这个selector监听的是服务端的
                //这里面的channel是每有个客户端都会有独立的一个所以是不同的
                log.debug("key:{}",key);

                //区分事件的类型
                if (key.isAcceptable()) { //如果是accept(连接)事件
                    //拿到触发事件的channel 肯定是只有服务端的才有accept事件所以转换成ServerSocketChannel,因为服务端也只关注accept事件
                    ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                    //处理事件来得到客户端的channel  假如我们把下面这行代码注释掉 则会进入循环
                    //因为没有执行accept()即不处理 selector对未处理的事件会重新把这个事件放回去,所以一直循环
                    SocketChannel sc = channel.accept();

                    //如果就是不想处理 可以取消这个事件 从selector集合中真正删除(物理删)
                    //key.cancel();

                    //设置客户端的channel为非阻塞
                    sc.configureBlocking(false);
                    //我们也想让客户端的channel可以像服务端那样 有事件了非阻塞 无事件就阻塞避免cpu空转
                    //那么同样的也得搞一个selector来管理channel,我们可以共用上面的selector 一个selector可以管理多个channel
                    //这里的sckey就是专门负责管理客户端的channel

                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    //第三个参数是附件的意思 设置了后 每个sckey都会有一个自己的附件 我们把buffer当作附件传递进来
                    SelectionKey sckey = sc.register(selector, 0, buffer);
                    //让客户端的key只关注于read事件
                    sckey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}",sc);  //客户端的channel
                    log.debug("sckey:{}",sckey); //客户端channel与selector注册的对象
                }else if(key.isReadable()){  //如果是read(读取)事件
                    //客户端跟服务端在连接的时候 如果客户端断开(无论是自己正常断开还是出问题了强制断开)客户端都会发送一个read事件关闭报文 这个是TCP协议要求的
                    //所以对于read这边得做一下特殊处理，不然channel.read(buffer)会有问  题，毕竟都断开连接了肯定读不到啊
                    //但是正常断开跟强制断开是与一些区别的 强制断开是抛异常 正常断开不会抛异常
                    try{
                        //拿到触发事件的channel 肯定是只有客户端的才有read事件所以转换成SocketChannel 因为客户端也只关注read事件
                        //而且肯定是在连接完后才能进行读写数据啦，所以这里的channel是已经走过上面accept的代码的了完成过连接的了
                        //且在上面accept代码中连接的时候被设置成非阻塞了
                        //所以这里的read就直接是非阻塞而且还能预防cpu空转的了 直接用即可
                        SocketChannel channel = (SocketChannel)key.channel();


                        //我们系统中设置的缓冲区的大小 跟 客户端发送的网络数据的长度 肯定是不能完全匹配的 所以还得处理一下消息边界的问题
                        //1.可以固定客户端发送消息的大小 然后我们缓冲区就也可以设置固定大小就行了  浪费带宽
                        //2.也可以按照数据格式区分比如数据之间用分隔符来分割，然后我们程序自己去通过分隔符处理  效率低下
                        //3.最好的方法是先计算一下数据的长度 然后 根据长度来创建buffer然后接收  netty就是这么帮我们实现的
                        //这里我们用第二种方式来处理 但是会有一些问题 问题描述参考else中的
                        //ByteBuffer buffer = ByteBuffer.allocate(16);

                        //获取附件虽然buffer的生命周期从局部变量变成了跟着sckey绑定但是还是有容量问题要处理
                        ByteBuffer buffer = (ByteBuffer) key.attachment();

                        //正常的read返回的是读取到的字节数,如果是正常断开/没数据了则返回的是-1
                        //正常断开时read事件的关闭报文是不代表被消费掉的所以正常断开情况下又会被selector返回去继续导致死循环
                        int read = channel.read(buffer);
                        if(read == -1){
                            key.cancel();
                        }else{
                            //用之前学的split方法来处理边接问题
//                            buffer.flip();
//                            debugRead(buffer);
                            //客户端发送超过缓冲区大小的数据 且在缓冲区大小内一直没出现分隔符就会导致只能读取到最后的数据 因为buffer是个局部变量
                            //读取完前面容量大小的数据后 没有遇到分隔符 依旧compact压缩了 然后进入下次循环，所以数据只能被抛弃掉 所以永远只能读取到最后的数据
                            //所以我们最好别用局部变量的方式 然后每个channel自己有一个buffer其实是最好的  而且bytebuff本身是线程不安全的所以跟每个单独的key绑定是最好的
                            split(buffer);

                            //在上面把buffer当作附件绑定了 所以现在的buffer是跟sckey的生命周期一样了 但是容量问题还是要解决
                            //netty里面也有这样的设计 但是netty的更好它会有个自适应的过程 因为bytebuffer肯定不能设计的很大 并发一上来内存就被吃完了 所以肯定得做成自适应的
                            if(buffer.position()==buffer.limit()){//调用了split方法后如果 指针位置跟最大的位置是一样的相当于中途没有遇到换行符进行消耗 代表满了
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2); //弄一个新的buffer当作是旧的buffer的两倍
                                buffer.flip(); //老的buffer只有切换成读的模式 新的buffer才能写进去
                                newBuffer.put(buffer);
                                key.attach(newBuffer); //重新绑定这个新的buffer 容量问题就解决了
                            }


                        }
                    }catch (IOException ex){
                        ex.printStackTrace();
                        //被强制断开时，直接取消这个TCP要求的关闭报文就好了  从selector集合中真正删除(物理删)
                        key.cancel();
                    }
                }

            }

        }

    }


    //其实逻辑就是遇到 \n的就把他当作一条数据整理一下 然后剩下的多出来的没遇到\n之前的把它当给下一条数据去处理
    private static void split(ByteBuffer source) {
        source.flip(); //切换成读模式
        for (int i = 0; i < source.limit(); i++) { //source.limit()实际可读的上限
            // 找到一条完整消息
            if (source.get(i) == '\n') {  // \n是换行 也是一个字节
                //索引是从0开始的 所以一般计算都是 end - start +1 就可用算出长度  这里的start就是Position
                int length = i + 1 - source.position();
                // 把这条完整消息存入新的 ByteBuffer
                ByteBuffer target = ByteBuffer.allocate(length);
                // 从 source 读，向 target 写
                for (int j = 0; j < length; j++) {
                    target.put(source.get());  //get()会移动读索引指针的位置 get(i)不会
                }
                debugAll(target);
            }
        }
        source.compact(); //压缩(把未读的数据留下已读过的数据删掉) 且 切换成写模式在未读之后继续写
    }


}
